openstack 中的WSGI

#1 WSGI 程序起步
本文中程序的放置路径及运行方式在 Window + Apache + WSGI 配置指明。

第一WSGI程序

1
2
3
4
5
6
7
8
9
def spch_wsgi(environ, start_response):
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return ['Hello World!']
application = spch_wsgi

WSGI server检索application函数, 并传递两个参数environ, start_response。
environ 为一个字典,包含环境变量。
start_response 为一个函数, 用于返回状态信息。

一个WSGI程序要完成两件事:
其一:返回HTTP header。本例中, 状态‘200 OK‘, 表明一切正常。
其二:返回一个iterable containing, 本例中是一个list。

输出environ信息

1
2
3
4
5
6
7
8
9
10
11
12
def application(environ, start_response):
response_body = ""
for k in environ:
tmp = "%s = %s \n" % (k, environ[k])
response_body += tmp
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return [response_body]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
wsgi.multiprocess = False
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = Apache/2.2.22 (Win32) mod_wsgi/3.3 Python/2.7.4
SCRIPT_NAME = /wsgi
mod_wsgi.handler_script =
SERVER_SIGNATURE =
REQUEST_METHOD = GET
PATH_INFO =
PATHEXT = .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
QUERY_STRING =
HTTP_USER_AGENT = Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
HTTP_CONNECTION = keep-alive
SERVER_NAME = localhost
REMOTE_ADDR = 127.0.0.1
mod_wsgi.request_handler = wsgi-script
wsgi.url_scheme = http
mod_wsgi.callable_object = application
SERVER_PORT = 80
mod_wsgi.version = (3, 3)
mod_wsgi.input_chunked = 0
SERVER_ADDR = 127.0.0.1
DOCUMENT_ROOT = D:/Program Files (x86)/Apache Software Foundation/Apache2.2/htdocs
mod_wsgi.process_group =
COMSPEC = C:\Windows\system32\cmd.exe
SCRIPT_FILENAME = C:/wsgi_app/wsgi_handler.py
SERVER_ADMIN = admin@localhost.com
wsgi.input = <mod_wsgi.Input object at 0x01379DE0>
HTTP_HOST = localhost
wsgi.multithread = True
SystemRoot = C:\Windows
REQUEST_URI = /wsgi
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
WINDIR = C:\Windows
wsgi.version = (1, 1)
GATEWAY_INTERFACE = CGI/1.1
wsgi.run_once = False
wsgi.errors = <mod_wsgi.Log object at 0x01379D40>
REMOTE_PORT = 64214
HTTP_ACCEPT_LANGUAGE = zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
mod_wsgi.application_group = 192.168.209.1|/wsgi
mod_wsgi.script_reloading = 1
wsgi.file_wrapper = <built-in method file_wrapper of mod_wsgi.Adapter object at 0x012E1770>
HTTP_ACCEPT_ENCODING = gzip, deflate

上述代码也可以通过类来实现,类中要重载call,这样的好处是可以从其它类继承,复用代码。

1
2
3
4
5
6
7
8
9
10
11
class MyApp:
def __call__(self, environ, start_response):
response_body = ['Hello World!']
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return response_body
application = MyApp()

#2 WSGI– Middleware
#
假定存在一个superSession模块,用于追踪用户访问行为。

1
2
3
4
5
6
7
8
9
import superSession
session = superSession.session()
print "Content-type: text/plain\n\n"
if session.has_key('visited'):
print "You have already visited!"
else:
session['visited'] = 1
print "This is your first visit."

上述代码创建了一个Session对象,追踪用户访问行为。将上述思想用于WSGI程序中。

1
2
3
4
5
6
7
8
9
10
def application(environ, start_response):
import superSession
session = superSession.session()
if session.has_key('visited'):
text = "You have already visited!"
else:
session['visited'] = 1
text = "This is your first visit."
start_response('200 OK', [('Content-type','text/plain')])
return [text]

可以将上述代码进行重构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def exampleApplication(environ, start_response):
if environ['superSession'].has_key('visited'):
text = "You have already visited!"
else:
environ['superSession']['visited'] = 1
text = "This is your first visit."
start_response('200 OK', [('Content-type','text/plain')])
return [text]
def session(application):
def app(environ, start_response):
if "superSession" not in environ:
import superSession
environ["superSession"] = superSession.session()
return application(environ, start_response)
return app
application = session(exampleApplication)

将session代码抽离放于session函数中,该函数专门用于判断用户访问行为。session函数将判断结果至于环境变量environ字典中。
exampleApplication通过environ字典获得用户访问行为。

我们称session函数为middleware,它处于server与application之间,对server传来的请求做相应的处理;它对于Server和application是透明的。
middleware的好处在于,通过middleware(本例中session函数)可以很简单的给WSGI程序添加新功能。

我们也可见将middleware包装成类,这样,我们可以通过继承,复用现有的中间件。类中要重载call

1
2
3
4
5
6
7
8
9
10
11
class Session:
def __init__(self, application):
self.application = application
def __call__(self, environ, start_response):
if "superSession" not in environ:
import superSession
environ["superSession"] = superSession.session() # Options would obviously need specifying
return self.application(environ,start_response)
application = Session(exampleApplication)

附录: 代码语法解释

1
2
3
4
5
6
7
8
9
def session(application):
def app(environ, start_response):
if "superSession" not in environ:
import superSession
environ["superSession"] = superSession.session()
return application(environ, start_response)
return app
application = session(exampleApplication)

将exampleApplication传入session函数,session函数中定义了一个新的函数app,session将app返回赋给application。
实际上相当于application = app。app函数中进行相应处理(superSession),将处理好的environ在传递给exampleApplication。

#3 webob request response

###Request

Webob的Request对象,提供对WSGI environ环境变量的包装,通过webob可以很容易的读写environ字典。
environ字典内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
TMP = C:\Users\spch2008\AppData\Local\Temp
PYTHONIOENCODING = GBK
COMPUTERNAME = SPCH2008
wsgi.multiprocess = False
PROCESSOR_LEVEL = 16
USERDOMAIN = SPCH2008
VS100COMNTOOLS = D:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\
HTTP_ACCEPT_LANGUAGE = zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
SERVER_PROTOCOL = HTTP/1.1
SERVER_SOFTWARE = WSGIServer/0.1 Python/2.7.4
PSMODULEPATH = C:\Windows\system32\WindowsPowerShell\v1.0\Modules\
SCRIPT_NAME =
COMMONPROGRAMFILES = C:\Program Files (x86)\Common Files
PROCESSOR_IDENTIFIER = AMD64 Family 16 Model 5 Stepping 3, AuthenticAMD
REQUEST_METHOD = GET
PROGRAMFILES = C:\Program Files (x86)
PROCESSOR_REVISION = 0503
PATH = D:/Program Files (x86)/java/jre7/bin/client;D:/Program Files (x86)/java/jre7/bin;D:/Program Files (x86)/java/jre7/lib/i386;C:\python32\;C:\python32\Lib\site-packages\;C:\python32\Scripts\;C:\Program Files (x86)\Common Files\NetSarang;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;D:\Program Files (x86)\java\jre7\bin;D:\Program Files (x86)\Rational\common;D:\Program Files (x86)\eclipse;
QUERY_STRING =
SYSTEMROOT = C:\Windows
PROGRAMFILES(X86) = C:\Program Files (x86)
PT5HOME = d:\Program Files (x86)\Cisco Packet Tracer 5.3.3
CONTENT_LENGTH =
HTTP_USER_AGENT = Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
HTTP_CONNECTION = keep-alive
TEMP = C:\Users\spch2008\AppData\Local\Temp
REMOTE_ADDR = 127.0.0.1
COMMONPROGRAMFILES(X86) = C:\Program Files (x86)\Common Files
PROCESSOR_ARCHITECTURE = x86
wsgi.url_scheme = http
ALLUSERSPROFILE = C:\ProgramData
PYDEV_CONSOLE_ENCODING = GBK
SERVER_PORT = 8080
LOCALAPPDATA = C:\Users\spch2008\AppData\Local
HOMEPATH = \Users\spch2008
USERDOMAIN_ROAMINGPROFILE = SPCH2008
PROGRAMW6432 = C:\Program Files
USERNAME = spch2008
HTTP_ACCEPT = text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
LOGONSERVER = \\MicrosoftAccount
PROMPT = $P$G
COMSPEC = C:\Windows\system32\cmd.exe
PROGRAMDATA = C:\ProgramData
PYTHONPATH = D:\Program Files (x86)\eclipse\plugins\org.python.pydev_2.7.3.2013031601\pysrc\pydev_sitecustomize;E:\GitHub\OpenStack\WSGI;C:\python32\DLLs;C:\python32\lib;C:\python32\lib\plat-win;C:\python32\lib\lib-tk;C:\python32;C:\python32\lib\site-packages
PATH_INFO = /
wsgi.multithread = True
wsgi.input = <socket._fileobject object at 0x0285C030>
wsgi.errors = <open file '<stderr>', mode 'w' at 0x01DA60D0>
HTTP_HOST = localhost:8080
SESSIONNAME = Console
PATHEXT = .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC
ASL.LOG = Destination=file
FP_NO_HOST_CHECK = NO
WINDIR = C:\Windows
wsgi.file_wrapper = wsgiref.util.FileWrapper
HTTP_ACCEPT_ENCODING = gzip, deflate
wsgi.version = (1, 0)
APPDATA = C:\Users\spch2008\AppData\Roaming
HOMEDRIVE = C:
SERVER_NAME = spch2008
wsgi.run_once = False
REMOTE_HOST = spch2008
SYSTEMDRIVE = C:
GATEWAY_INTERFACE = CGI/1.1
PYDEV_COMPLETER_PYTHONPATH = D:\Program Files (x86)\eclipse\plugins\org.python.pydev_2.7.3.2013031601\pysrc
NUMBER_OF_PROCESSORS = 4
DJANGO_SETTINGS_MODULE = WSGI.settings
CONTENT_TYPE = text/plain
PROCESSOR_ARCHITEW6432 = AMD64
COMMONPROGRAMW6432 = C:\Program Files\Common Files
OS = Windows_NT
PUBLIC = C:\Users\Public
USERPROFILE = C:\Users\spch2008
1
req = Request(environ)

通过Request操作上述环境变量,所得结果如下:

req.method ‘Get’
req.path_info ‘/‘
req.content_type ‘text/plain’
req.remote_user ‘None’
req.host ‘localhost:8080’
即通过req,可以很方便的读取environ环境变量,更多操作请看:http://docs.webob.org/en/latest/modules/webob.html

##Response

Response包含了所有响应WSGI Server需要的变量。

1
2
3
4
res = Response()
res.status = 200
res.headerlist = [('Content-type', 'text/html')]
res.body = 'Hello World!'

使用webob改写之前的Hello World程序。j将上述代码粘贴到eclipse中,运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from wsgiref.simple_server import make_server
from webob import Request, Response
class MyApp:
def __call__(self, environ, start_response):
req = Request(environ)
res = Response()
res.status = 200
res.headerlist = [('Content-Type', 'text/plain')]
res.body = "Hello World!"
return res(environ, start_response)
application = MyApp()
httpd = make_server('localhost', 8080, application)
httpd.serve_forever()

4 Webob WSGI 装饰器wsgify装饰器将一个普通函数转变成WSGI应用程序。
class webob.dec.wsgify(func=None, RequestClass=None, args=(), kwargs=None, middleware_wraps=None)

小示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from wsgiref.simple_server import make_server
from webob import Request, Response
from webob.dec import *
@wsgify
def test(req):
res = Response()
res.status = 200
res.body = "spch"
return res
class MyApp:
def __call__(self, environ, start_response):
req = Request(environ)
return test(environ, start_response)
application = MyApp()
httpd = make_server('localhost', 8081, application)
httpd.serve_forever()

其中,参数req为一个Request实例,可以通过req读取相应环境变量。

而且,我们可以定制装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from wsgiref.simple_server import make_server
from webob import Request, Response
from webob.dec import *
from webob.exc import *
class MyRequest(Request):
@property
def is_local(self):
return self.remote_addr == '127.0.0.1'
@wsgify(RequestClass=MyRequest)
def myfunc(req):
if req.is_local:
return Response('hi!')
else:
raise HTTPForbidden
class MyApp:
def __call__(self, environ, start_response):
req = Request(environ)
return myfunc(environ, start_response)
application = MyApp()
httpd = make_server('localhost', 8081, application)
httpd.serve_forever()

如何是本机访问,则输出’hi‘,否则不允许
5 Routes 起步

1
2
3
4
5
6
7
from routes import Mapper
map = Mapper()
map.connect('spch', '/blog', controller='main', action='index')
result = map.match('/blog')
print result
{'action': u'index', 'controller': u'main'}

1.2 行创建一个mapper

  1. 行注册一条路由, 路由名称为’spch’, 路径为’/blog’, controller为main,
    action为index
    可以这样认为,匹配到此条路由的请求交由controller处理,请求预调用的
    函数为index

  2. 创建好路由条目后,即可以进行匹配,调用match方法,匹配路径’blog’

  3. 输出匹配结果
    1
    2
    3
    4
    map.connect(None, "/error/{action}/{id}", controller="error")
    result = map.match('/error/index/2')
    print result
    {'action': u'index', 'controller': u'error', 'id': u'2'}

1.注册了一条无名路由,并且action从匹配路由中获得
同样,我们可以省掉None
map.connect(“/error/{action}/{id}”, controller=”error”)
上述语句同样注册了一条无名路由。

Conditions
Conditions用于限制进行路由匹配,比如method

1
2
3
4
5
6
7
8
9
10
m.connect("/user/list", controller="user", action="list", conditions=dict(method=["GET", "HEAD"]))
``` 
只匹配GET,HEAD请求。
Requirements
有时只想匹配数字,或者匹配可选的几个条目
```python
map.connect(R"/blog/{id:\d+}")
map.connect(R"/download/{platform:windows|mac}/{filename}")

\d表示匹配1位数字,\d+表示匹配多位
windows|mac 表示只匹配windows或者mac
可以将上述写成

1
2
3
map.connect("/blog/{id}", requirements={"id": R"\d+"}
map.connect("/download/{platform}/{filename}",
requirements={"platform": R"windows|mac"})

Format extensions
通过{.format}来指定匹配格式

1
2
3
4
5
map.connect('/entries/{id}{.format}')
print map.match('/entries/2')
{'id': u'2', 'format': None}
print map.match('/entries/2.mp3')
{'id': u'2', 'format': u'mp3'}

1
2
3
4
5
6
7
map.connect('/entries/{id:\d+}{.format:mp3}')
print map.match('/entries/2.mp3')
{'id': u'2', 'format': u'mp3'}
print map.match('/entries/2')
{'id': u'2', 'format': None}
print map.match('/entries/2.mp4')
None

注意:{id:\d+}, 如果没有\d+, print map.match(‘/entries/2.mp4’)将输出 {‘id’: u’2.mp4’, ‘format’: None}是可以成功的。
有了\d+后,由于没有匹配format,同时\d+要求只匹配数字,所有2.mp4匹配失败

6 Routes Resource当路由条目过多时,需要一条一条注册,过于麻烦,此时可以通过resource route简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
map.connect("messages", "/messages",
controller="messages", action="create",
conditions=dict(method=["POST"]))
map.connect("messages", "/messages",
controller="messages", action="index",
conditions=dict(method=["GET"]))
map.connect("formatted_messages", "/messages.{format}",
controller="messages", action="index",
conditions=dict(method=["GET"]))
map.connect("new_message", "/messages/new",
controller="messages", action="new",
conditions=dict(method=["GET"]))
map.connect("formatted_new_message", "/messages/new.{format}",
controller="messages", action="new",
conditions=dict(method=["GET"]))
map.connect("/messages/{id}",
controller="messages", action="update",
conditions=dict(method=["PUT"]))
map.connect("/messages/{id}",
controller="messages", action="delete",
conditions=dict(method=["DELETE"]))
map.connect("edit_message", "/messages/{id}/edit",
controller="messages", action="edit",
conditions=dict(method=["GET"]))
map.connect("formatted_edit_message", "/messages/{id}.{format}/edit",
controller="messages", action="edit",
conditions=dict(method=["GET"]))
map.connect("message", "/messages/{id}",
controller="messages", action="show",
conditions=dict(method=["GET"]))
map.connect("formatted_message", "/messages/{id}.{format}",
controller="messages", action="show",
conditions=dict(method=["GET"]))

上述路由条目可以使用这一条语句代替。

1
map.resource("message", "messages")

两个参数,一个指定单数,为member路由名字;一个指定复数,为collection路由名字。
函数原型:resource(member_name, collection_name, **kwargs)

1
2
3
4
5
6
7
GET /messages => messages.index() => url("messages")
POST /messages => messages.create() => url("messages")
GET /messages/new => messages.new() => url("new_message")
PUT /messages/1 => messages.update(id) => url("message", id=1)
DELETE /messages/1 => messages.delete(id) => url("message", id=1)
GET /messages/1 => messages.show(id) => url("message", id=1)
GET /messages/1/edit => messages.edit(id) => url("edit_message", id=1)

这里有必要说一下member 路由与 collection路由。
上述的路由模型

1
2
3
4
5
6
7
GET /messages => messages.index()
POST /messages => messages.create()
GET /messages/new => messages.new()
PUT /messages/1 => messages.update(id)
DELETE /messages/1 => messages.delete(id)
GET /messages/1 => messages.show(id)
GET /messages/1/edit => messages.edit(id)

  1. 有的路由有id, 指向一个具体的对象
  2. 有的路由没有id, 指向全体对象
  3. 有的路由(index/create, show/update/delete)有相同的URL,但是HTTP method不同
  4. 有的路由(show/edit)HTTP method和前缀相同,仅后缀不同

一个member路由指定具体实例,也就是说它们有id。而一个collection路由,
没有指定的实例,即没有给定id
综上:member路由操作一个单独的实例,而collection操作全体实例。

另一个函数collection也可以完成上述功能。
函数原型:collection(collection_name, resource_name, path_prefix=None, member_prefix=’/{id}’, controller=None, collection_actions=[‘index’, ‘create’, ‘new’],member_actions=[‘show’, ‘update’,
‘delete’, ‘edit’], member_options=None, **kwargs)

用法:
map.collection(‘entries’, ‘entry’)

7 Routes RoutesMiddlewareRoutesMiddleware将请求应声到相应WSGI程序,它将路由匹配结果存到environ环境变量中去。

1
2
from routes.middleware import RoutesMiddleware
app = RoutesMiddleware(wsgi_app, map) # ``map`` is a routes.Mapper.

map调用match匹配URL,并设置WSGI环境变量

1
2
3
environ['wsgiorg.routing_args'] = ((url, match))
environ['routes.route'] = route
environ['routes.url'] = url

route为匹配到的路由,url为一个URLGenerator对象,match为匹配所得条目。

app为一个RoutesMiddleware对象,内部重载call(def call(self, environ, start_response))仍为一个wsgi应用。
wsgi_app为一个wsgi程序,RoutesMiddleware将环境变量(environ)设置好后,调用wsgi_app进行后续处理。

下面是一个实际的输出:

1
2
3
4
wsgiorg.routing_args = (<routes.util.URLGenerator object at 0x0287AFB0>,
{'action': u'index', 'controller': <__main__.Resourse instance at 0x02876E40>})
routes.route = <routes.route.Route object at 0x02871F10>
routes.url = <routes.util.URLGenerator object at 0x0287AFB0>

8 WSGI Webob Routes 实例1.下载库文件
webob库:http://download.csdn.net/detail/spch2008/5497755
routes库:http://download.csdn.net/detail/spch2008/5497757
repoze库:http://download.csdn.net/detail/spch2008/5499231

  1. 组织代码
  2. 代码
'''
Created on 2013-6-1

@author: spch2008
'''

from wsgiref.simple_server import make_server

import routes.middleware
import webob.dec
import webob.exc

class Controller:
    @webob.dec.wsgify
    def __call__(self, req):
        return webob.Response("Hello World!")



class Router(object):
    def __init__(self):
        self._mapper = routes.Mapper()
        self._mapper.connect('/spch',  
                        controller=Controller(),  
                        action='index',  
                        conditions={'method': ['GET']})  

        self._router = routes.middleware.RoutesMiddleware(self._dispatch, self._mapper)

    @webob.dec.wsgify
    def __call__(self, req):

        return self._router

    @staticmethod
    @webob.dec.wsgify
    def _dispatch(req):
        match = req.environ['wsgiorg.routing_args'][1]

        if not match:
            return webob.exc.HTTPNotFound()

        app = match['controller']  
        return app



app = Router()
httpd = make_server('localhost', 8282, app)  
httpd.serve_forever()

22行:创建一个mapper
23行:#注册一个路由
28行:创建一个RoutesMiddleware对象,匹配路由,修改环境变量后,调用self._dispatch

  1. 运行结果
Jerky Lu wechat
欢迎加入微信公众号